package com.conversationboard.security;

import java.io.IOException;
import java.security.NoSuchAlgorithmException;
import java.util.Date;

import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;

import sun.misc.BASE64Decoder;
import sun.misc.BASE64Encoder;

import com.conversationboard.config.Configuration;

/**
 * 
 * Class to generate a key from an input seed stored in the site's
 * Configuration.getInstance(). Include functionality to decode the key into its constituent
 * parts.
 * 
 * Used to ensure that external users cannot construct URLs or post requests via
 * an XSS approach to faciliate admin actions. It works like this:
 * 
 * 1. An admin tries to say, delete a thread 2. This directs them to a confirm
 * page - an encrypted key is generated and delivered on the confirm page as a
 * hidden input field on the form. 3. The admin confirms and is directed to the
 * servlet that does the actual work 4. The servlet gets the key from the form,
 * decrypts it; checks that the seed is what was put in, and finally checks that
 * it was submitted no more than (say) five minutes ago; this makes it difficult
 * to cause problems even if an attacker can get access to an encrypted field
 * from an admin's browser. Obviously, if any of these checks fail, the admin
 * action is thrown out.
 * 
 * 
 * @author Keith Watson
 * 
 */

public class EncryptedKey implements IEncryptedKey {

	private static final int EXPIRY_TIME_IN_MILLISECONDS = 300000;
	private static final String ALGORITHM = "Rijndael";

	private static SecretKey secretKey = null;

	static {
		try {
			KeyGenerator keyGenerator = KeyGenerator.getInstance(ALGORITHM);
			secretKey = keyGenerator.generateKey();
		} catch (NoSuchAlgorithmException e) {
			/* No such algorithm exception shouldn't really be a checked exception */
		}
	}


	/**
	 * Get a new key by passing in the site seed. Encrypt the key and base 64
	 * encode it.
	 * 
	 * Key looks like:
	 * 
	 * Site Seed\n Current date and time\n Login id
	 * 
	 */

	public String getKey(String loginId) throws EncryptionException {

		String siteSeed = Configuration.getInstance().getKey();
		long currentDateTime = new Date().getTime();

		String preEncoded = siteSeed + "\n" + currentDateTime + "\n" + loginId.toLowerCase();

		return this.encrypt(preEncoded);
	}


	/**
	 * Returns a key that doesn't encode the user ID. Useful for use with the registration
	 * page, where the user is not logged in.
	 * 
	 * @return
	 * @throws EncryptionException
	 */

	public String getKey() throws EncryptionException {

		String siteSeed = Configuration.getInstance().getKey();
		long currentDateTime = new Date().getTime();

		String preEncoded = siteSeed + "\n" + currentDateTime + "\n";

		return this.encrypt(preEncoded);

	}


	/**
	 * Pass in the key obtained from the user and the site seed and check if
	 * they're still valid.
	 */

	public boolean isValid(String key, String loginId) throws EncryptionException {

		/* Get the constituent parts of the key */

		String decodedKey = this.decrypt(key);

		String[] parts = decodedKey.split("\n");
		String seed = parts[0];
		long keyDateTime = Long.parseLong(parts[1]);
		String keyLoginId = parts[2];

		String siteSeed = Configuration.getInstance().getKey();

		if (seed.equals(siteSeed) && loginId.equalsIgnoreCase(keyLoginId)) {
			long currentDateTime = new Date().getTime();

			if ((currentDateTime - keyDateTime) < EXPIRY_TIME_IN_MILLISECONDS) {
				return true;
			}
		}

		return false;
	}


	public boolean isValid(String key) {
		return this.isValid(key, EXPIRY_TIME_IN_MILLISECONDS);
	}


	/**
	 * Version of the above method that checks out a key that doesn't have a user's ID
	 * encoded in it. Used for functions when the user is not logged in.
	 * 
	 * @param key
	 * @return
	 * @throws EncryptionException
	 */

	public boolean isValid(String key, int expiryTimeInMinutes) {

		try {
			String decodedKey = this.decrypt(key);

			String[] parts = decodedKey.split("\n");
			String seed = parts[0];
			long keyDateTime = Long.parseLong(parts[1]);

			String siteSeed = Configuration.getInstance().getKey();

			if (seed.equals(siteSeed)) {
				long currentDateTime = new Date().getTime();

				if ((currentDateTime - keyDateTime) < (expiryTimeInMinutes * 60000)) {
					return true;
				}
			}
		} catch (EncryptionException e) {
			return false;
		}

		return false;

	}


	/**
	 * Read the secret key from a text file. Do it only once and hold it
	 * statically.
	 * 
	 * @return
	 * @throws IOException
	 * @throws ClassNotFoundException
	 */

	private SecretKey getEncryptionKey() {
		return secretKey;
	}


	/**
	 * Encrypt and Base 64 encode the input message and return it as a String.
	 * 
	 * @param message
	 * @return
	 * @throws EncryptionException
	 */

	private String encrypt(String message) throws EncryptionException {

		try {

			SecretKey key = this.getEncryptionKey();

			byte[] messageBytes = message.getBytes();

			Cipher cipher = Cipher.getInstance(ALGORITHM);
			cipher.init(Cipher.ENCRYPT_MODE, key);

			byte[] encrypted = cipher.doFinal(messageBytes);

			BASE64Encoder encoder = new BASE64Encoder();
			String encodedEncrypted = encoder.encode(encrypted);

			return encodedEncrypted;

		} catch (Exception e) {
			throw new EncryptionException(e);
		}

	}


	/**
	 * Expects a message sent in Base 64 encoded and encrypted using ALGORITHM.
	 * It will decode and decrypt this message and return the original as a
	 * String.
	 * 
	 * @param message
	 * @return
	 */

	private String decrypt(String message) throws EncryptionException {

		try {

			BASE64Decoder decoder = new BASE64Decoder();
			byte[] decoded = decoder.decodeBuffer(message);

			Cipher cipher = Cipher.getInstance(ALGORITHM);
			cipher.init(Cipher.DECRYPT_MODE, this.getEncryptionKey());

			byte[] decrypted = cipher.doFinal(decoded);

			return new String(decrypted);

		} catch (Exception e) {
			throw new EncryptionException(e);
		}

	}

}
